2.6 类型系统的安全与限制

在之前的章节中,我们讲到了基础数据类型的内存布局、动态类型的底层结构及内存分配、切片及map的底层结构及原理。

本节我们将针对类型系统的安全及限制进行探讨,主要针对设计原则、安全性、灵活性以及局限性进行探讨。

本节代码存放目录为 lesson6

类型系统设计原则

Go语言的类型系统是比较简洁、安全的,我们将进一步探讨它的原因,这样也能够帮助我们更好的理解它的核心。

静态类型

在之前的章节中我们讲过了静态类型与动态类型,静态类型指的就是在编译的时候就已经确定了,并且在整个程序的运行周期内是不会产生变化的。

Go语言中,在编译阶段变量、常量的类型在声明时就已经被明确指定或者断言了,并且在编译阶段会进行类型检查。

var a int
a = 10
fmt.Printf("a: %d\n", a)

在上面的示例中,在声明的时候a变量的类型就已经被确定为了int,之后a变量就只能存储int值。

在编译的时候编译器就会检查a变量的类型是否正确,以及检查在整个程序内的调用赋值是否正常。

因为类型在编译时已经确定,编译器可以在编译阶段捕捉到类型相关的错误,这样可以避免一些运行时错误,提高程序的安全性和可靠性。

静态类型使得代码的类型信息明确,从而提高了代码的可读性和可维护性。


强类型

强类型是指语言在处理变量类型时,要求严格的类型一致性,禁止隐式的类型转换。

换句话说,不同类型之间的操作必须显式转换,以避免因自动转换导致的潜在错误。

var b float64 = 5.5
c := float64(a) + b
fmt.Printf("c: %0.2f\n", c)

在上面的示例中,aint类型,bfloat64类型,那么此时需要执行计算操作,就必须将ab的类型转换为一致的,否则就会编译报错。

可以选择将a转换为float64,也可以选择将b转换为int,总之类型必须保持一致,这就是强类型的表现。

强类型系统避免了隐式类型转换带来的意外行为,使我们的程序的行为更加可预测,也就是所见即所得,避免了很多的调试工作。

强类型系统要求我们显式地处理类型转换,从而增强了程序的类型安全性,减少了因为类型问题导致的运行时错误。


简洁易用

Go语言的类型系统设计遵循简洁和易用的原则,减少不必要的复杂性,使代码更易读、更易维护。

官方包也对类型转换做了很好的支持,我们可以很方便的进行类型转换处理,对于静态型语言来说,这是比较友好的。

同时Go语言可以在编译阶段进行类型推断,这样也使得我们的开发速度能够进一步提升,也能够减少代码的错误。

c := float64(a) + b

比如在上面的例子中,我们不需要先去指定c的类型,而是通过:=就可以进行类型推断,这显然是很方便的。


类型的明确性

在上面我们也已经提到,强类型是Go语言的一个特性,类型都是显式的,我们可以通过类型清晰的表达意图,避免了很多由于隐式类型转换导致的错误。

在进行类型转换时,也提供了安全的显示类型转换机制,避免了在进行转换时出现各种崩溃、错误等。

总的来说,Go语言在它的领域,其强类型、明确类型等为开发者提供了很大的便利,同时也让这门语言更容易入门。

类型系统的安全性

编译时类型检查

Go语言的编译器在编译时会对所有的类型进行严格的检查,确保变量、函数参数、返回值等的一致性。

这有助于在开发阶段就捕获到潜在的类型错误,减少运行时错误的发生。


接口与类型安全

在空接口中,我们之前讲到了它的底层结构,底层存储了实际的数据类型type。这就使得我们在获取实际值以及进行类型转换时变得很方便及安全。

我们可以通过.type获取到实际类型进行断言,从而进行相应的转换。如下代码所示:

var d interface{}
d = 100
switch d.(type) {
case int:
    fmt.Printf("d: int %d\n", d.(int))
case string:
    fmt.Printf("d: string %d\n", d.(string))
default:
    fmt.Printf("unknow")
}

在上面的代码中,使用switch对数据类型进行了断言,这就不会导致在取值时出现崩溃,使用了一种更安全的方式。

在非空接口中,我们之前讲到了他的接口表、方法表,在编译时就会对类型信息进行确认及关联。

只有完全实现了接口的所有方法,那么实际的结构体类型才能够与接口绑定起来,这也进一步加强了它的安全性,避免出现运行时错误。


零值安全

Go语言的零值(0nilfalse"")等设计保证了在声明但是未初始化变量时,变量仍然具有安全的默认值,避免了空指针引用等常见的运行时错误。

对于引用类型mapchannel等,Go语言也提供了明确的make方法,让我们可以安全的实现功能,不用太去关注初始化、内存分配等内容。

类型系统的灵活性

接口的灵活性

在之前的章节中,我们已经讲过了接口实现多态的机制。让我们可以在不修改现在代码的情况下就可以实现扩展及模块化设计,这对于一些大型系统显然是很有用的。

Go语言在接口方面实现了鸭子类型的特性,鸭子类型指的是:如果它像鸭子一样走路,并且像鸭子一样叫,那么它就是一只鸭子

如下代码所示:

type Animal interface {
    Speak()
}

type Dog struct {
    Name  string
    Voice string
}

func (d *Dog) Speak() {
    fmt.Printf("Name is %s, Speak-> %s\n", d.Name, d.Voice)
}

var animalDog Animal = &Dog{
    Name:  "Dog",
    Voice: "Wang...",
}
animalDog.Speak()

在上面的代码中,Dog就属于是像Animal一样走路,像Animal一样叫。因为Animal有的SpeakDog同样是具有的。

所以Dog也就可以认为是Animal,但是鸭子类型其实是不安全的,因为它依赖于运行时的行为判断,编译器无法在编译时捕获某些类型错误。

但在Go中,虽然接口支持鸭子类型的灵活性,但由于静态类型检查,Go能够在编译时保证类型安全。


类型推断与泛型

类型推断在上文我们已经讲解过,通过简单的:=就可以推断出实际的数据类型。

而泛型则是Go1.18才推出的东西,早在Go1.10版本之前,很多开发者就提出了泛型的建议,但是一直到Go1.18才推出了泛型。

泛型对于Go语言本身的强类型还是有一些冲击的,同时对于编译性能也有一些影响,但这个问题在Go1.20解决了一些,之后可能还会继续优化。

使用方法如下所示:

func Print[T any](text T) {
    fmt.Printf("Print: %v\n", text)
}

Print(1)
Print("hello...")
Print(1.0989)

在上面的代码中,我们将参数定义为了any类型,也就是可以接收任意类型的参数。

关于泛型更多的信息,我们会在后续章节进行讲解,这里不做阐述。


类型别名

类型别名是Go语言的一大特性,简单来说就是允许为类型重新定义一个名字。

如下代码所示:

type newInt = int
var e newInt
e = 1
fmt.Printf("e: %d\n", e)

在上面的代码中,我们将int重新定义为了newInt,也就是说newInt现在是等同于 int的。

那么为什么要这样做呢,这样做有什么意义?

类型别名一般我们可以用于大型的系统中,比如说我有一个字段,是int类型,现在我需要将这个字段在所有地方都改为int64类型。

那么这时候改动起来就会比较复杂,但是如果我使用了别名,在开发的时候在部分功能中,将这个字段的类型定义为了别名:newInt

那现在只需要全部将newInt的别名定义var newInt int更新为var newInt int64即可。这就是一个直观的使用方法。

小结

本节我们主要针对Go语言类型系统做了一个分析,包括静态类型、强类型、类型安全等内容。

关于本节总结如下:

  • 静态类型就是编译时就确定好,之后不会再改变的

  • 强类型就是要求在类型转换时必须是显式进行的,确保了不同类型间的操作具有明确性和安全性

  • 编译时类型检查提高了类型的安全性

  • 接口、泛型提高了代码的灵活性

results matching ""

    No results matching ""